# 快速入门

欢迎来到 LCUI 文档！本章节将介绍一些基础概念和用法，以帮助你快速入门。

:::note
你将会学习到：

- 如何创建应用程序
- 如何创建和嵌套组件
- 如何创建自定义组件
- 如何设置工作目录以让程序正确加载文件
- 如何调整布局
- 如何使用 XML 描述界面
- 如何对事件做出响应并更新界面
:::

## 在你开始之前

本文档面向有开发经验的中级开发者，且具备以下条件：

- 熟练掌握 C 语言及构建工具链，能够解决常见的编译问题。
- 熟悉至少一个 GUI 开发库/框架，理解 GUI 应用程序工作原理。
- 了解 Web 开发技术，包括 HTML、CSS。

## 安装 LCUI

阅读《[安装](./2-installation.md)》文档。

## 创建应用程序

创建 main.c 文件，写入如下代码：

```c title="src/main.c"
#include <LCUI.h>
#include <LCUI/main.h>

int main(int argc, char *argv[])
{
        lcui_init();
        return lcui_main();
}
```

这段代码比较简单，主要是包含头文件和调用函数，其作用具体如下：

- LCUI.h 是 LCUI 库的头文件。
- LCUI/main.h 是则是程序主入口的头文件，仅需由 main 函数所在的源文件包含，它封装了各个平台的程序入口代码，使得 LCUI 应用程序能够统一用 main 函数作为入口。
- 调用 `lcui_init()` 初始化 LCUI 库的各项功能。
- 调用 `lcui_main()` 进入主循环，让应用程序保持运行状态并能够响应用户操作。

## 创建和嵌套组件

LCUI 应用程序的用户界面是由**组件**组成的。组件拥有自己的逻辑和外观，可以小到一个按钮，也可以大到整个页面。

接下来，我们将使用 text 组件来显示一段文本：

```c title="src/main.c"
#include <LCUI.h>
#include <LCUI/main.h>

int main(int argc, char *argv[])
{
        lcui_init();

        // highlight-start
        ui_widget_t *text = ui_create_widget("text");
        ui_text_set_content(text, "Welcome to my app");
        ui_root_append(text);
        // highlight-end

        return lcui_main();
}
```

在这段代码中我们创建了一个 text 类型的组件，将它的内容设置为 `"Welcome to my app"`，然后追加到根组件中。

运行命令 `xmake && xmake run` 重新构建和运行它，你会看到这样的窗口：

import Window from "@site/src/components/QuickStart/Window";

<Window>Welcome to my app</Window>

:::note
在默认的显示模式下，根组件与主窗口绑定，它的尺寸、内容都会同步到主窗口，这意味着将组件追加到根组件内就能让它在窗口中显示。
:::

## 创建自定义组件

LCUI 的组件基于原型来实现组件的抽象和继承，组件原型记录了组件的创建、销毁、绘制、估算尺寸等方法，通过创建组件原型然后修改这些方法即可创建你的自定义组件。

我们将添加一个名为 `my-button` 的自定义组件，它的表现形式是按钮，初始内容是 `I'm a button`。

首先定义原型指针和注册函数：

```c title="src/main.c"
ui_widget_prototype_t *my_button_proto;

void register_my_button(void)
{
        my_button_proto = ui_create_widget_prototype("my-button", "text");
        my_button_proto->init = my_button_init;
}
```

`ui_create_widget_prototype()` 用于创建组件原型，它参数分别是组件的类名和基类名。因为 `my-button` 组件需要用到文本显示能力，所以将第二个参数设置为 `"text"` 以继承 `text` 组件。

然后定义初始化函数，在里面初始化基组件和自身内容：

```c title="src/main.c"
void my_button_init(ui_widget_t *w)
{
        my_button_proto->proto->init(w);
        ui_text_set_content(w, "I'm a button");
}
```

最后，在主函数中注册和使用我们的自定义组件：

```c title="src/main.c"
#include <LCUI.h>
#include <LCUI/main.h>

ui_widget_prototype_t *my_button_proto;

void my_button_init(ui_widget_t *w)
{
        my_button_proto->proto->init(w);
        ui_text_set_content(w, "I'm a button");
}

void register_my_button(void)
{
        my_button_proto = ui_create_widget_prototype("my-button", "button");
        my_button_proto->init = my_button_init;
}

int main(int argc, char *argv[])
{
        lcui_init();
        // highlight-next-line
        register_my_button();

        ui_widget_t *text = ui_create_widget("text");
        ui_text_set_content(text, "Welcome to my app");
        ui_root_append(text);
        // highlight-next-line
        ui_root_append(ui_create_widget("my-button"));

        return lcui_main();
}
```

重新构建和运行它，你会看到如下的效果：

import Button from "@site/src/components/QuickStart/Button";

<Window>
  <div>Welcome to my app</div>
  <div>I'm a button</div>
</Window>

## 设置工作目录

接下来要让程序从外部加载资源文件。在代码中加载文件时指定的文件路径是相对路径，相对于工作目录，且会因程序的启动方式的不同而有所差异，例如：用 `xmake run` 命令启动、双击程序的可执行文件启动。因此，在加载文件前需手动设置工作目录。

在下面的例子中我们将 app 目录作为工作目录，需要用到 `set_rundir()` 函数：

```lua title="xmake.lua"
add_rules("mode.debug", "mode.release")
includes("vendor/LCUI/xmake.lua")

target("myapp")
  set_kind("binary")
  add_deps("lcui")
  add_files("src/*.c")
  -- highlight-next-line
  set_rundir("app")
```

:::warning
这项配置只影响到以 `xmake run` 命令启动程序时的工作目录，你还需要额外编写代码根据 `main()` 函数的 argv 参数设置工作目录。
:::

## 添加样式

LCUI 包含 CSS 解析和选择引擎，你可以使用一些简单的 CSS 规则来设置用户界面的视觉效果。

你将学会使用这几个函数：

- `ui_load_css_file()`：从文件中加载 CSS。
- `ui_load_css_string()`：从字符串中加载 CSS。
- `ui_widget_add_class()`：为组件设置类名，以应用对应类的 CSS 样式。

首先在工作目录内创建一个 css 文件，填入以下 CSS 规则：

```css title="app/main.css"
root {
  padding: 24px;
}

.title {
  font-size: 24px;
  font-weight: bold;
  margin-bottom: 16px;
}

my-button {
  display: inline-block;
  padding: 4px 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
  background: #f4f4f4;
}

my-button:hover {
  background: #eaeaea;
}

my-button:active {
  background: #fbfbfb;
}
```

然后在 `main()` 函数中加载 CSS，再为 text 组件添加 title 类名。

```c title="src/main.c"
int main(int argc, char *argv[])
{
        lcui_init();
        register_my_button();

        // highlight-next-line
        ui_load_css_file("main.css");

        ui_widget_t *text = ui_create_widget("text");
        ui_text_set_content(text, "Welcome to my app");
        // highlight-next-line
        ui_widget_add_class(text, "title");
        ui_root_append(text);
        ui_root_append(ui_create_widget("my-button"));

        return lcui_main();
}
```

效果如下所示：

<Window>
  <div style={{ padding: 24 }}>
    <div style={{ fontSize: 24, fontWeight: "bold", marginBottom: 16 }}>
      Welcome to my app
    </div>
    <Button>I'm a button</Button>
  </div>
</Window>

除了从文件中加载 CSS，你还可以从字符串中加载 CSS：

```c title="src/main.c"
// highlight-start
const char *css_str = "\
root {\
  padding: 24px;\
}\
\
.title {\
  font-size: 24px;\
  font-weight: bold;\
  margin-bottom: 16px;\
}\
my-button {\
  padding: 4px 8px;\
  border: 1px solid #ddd;\
  border-radius: 4px;\
  background: #f4f4f4;\
}\
my-button:hover {\
  background: #eaeaea;\
}\
my-button:active {\
  background: #fbfbfb;\
}";
// highlight-end

int main(int argc, char *argv[])
{
        lcui_init();
        register_my_button();

        // highlight-next-line
        ui_load_css_string(css_str, __FILE__);

        ui_widget_t *text = ui_create_widget("text");
        ui_text_set_content(text, "Welcome to my app");
        ui_widget_add_class(text, "title");
        ui_root_append(text);
        ui_root_append(ui_create_widget("my-button"));

        return lcui_main();
}
```

## 调整布局

LCUI 支持流式布局（Flow Layout）和弹性盒子布局（Flexible Box）两种布局，用法和效果与网页的布局相似，你可通过设置组件的 CSS display 属性来更改其布局行为。

以常见的垂直水平居中布局为例，我们可以使用弹性盒子布局来实现：

```css title="app/main.css"
root {
  padding: 24px;
  // highlight-start
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  // highlight-end
}

.title {
  font-size: 24px;
  font-weight: bold;
  margin-bottom: 16px;
}
```

<Window>
  <div style={{ padding: 24, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
    <div style={{ fontSize: 24, fontWeight: "bold", marginBottom: 16 }}>
      Welcome to my app
    </div>
    <Button>I'm a button</Button>
  </div>
</Window>

## 使用 XML 描述界面

使用 C 语言以命令式编写用户界面虽然具有很高的灵活性，但当界面变得复杂时，可读性会随着函数调用和参数的数量增多而降低，界面的结构也变得难以理解。相比之下，使用 XML 声明用户界面则能更直观地表达界面结构、各个组件的属性和嵌套关系。

XML 写法如下：

```xml title="app/main.xml"
<?xml version="1.0" encoding="UTF-8" ?>
<lcui-app>
  <resource type="text/css" src="main.css"/>
  <ui>
    <text class="title">
      Welcome to my app
    </text>
    <my-button />
  </ui>
</lcui-app>
```

resource 标签用于引入包括 CSS、字体在内的资源文件。ui 标签则用于声明用户界面内的所有组件。

在 `main()` 函数中调用 `ui_load_xml_file()` 函数加载这个 XML 文件：

```c title="src/main.c"
int main(int argc, char *argv[])
{
        lcui_init();
        register_my_button();

        // highlight-start
        ui_widget_t *result = ui_load_xml_file("main.xml");
        if (result == NULL) {
          return -1;
        }
        ui_root_append(result);
        ui_widget_unwrap(result);
        // highlight-end
        return lcui_main();
}
```

`ui_load_xml_file()` 函数返回一个组件，包含 ui 标签内声明的所有组件，你需要将它追加到目标容器（根组件）内再调用 `ui_widget_unwrap()` 函数将内部的子组件展开到外层。

:::caution
libxml2 库的体积较大，我们计划在 LCUI 未来的版本中移除对该库的依赖。
:::

:::note
推荐使用 [@lcui/cli](https://gitee.com/lcui-dev/lcui-cli) 工具将 xml 文件编译为 C 源码。
:::

## 响应事件

LCUI 的组件是通过事件来实现与用户交互的。接下来让我们给应用程序添加一个交互效果，在按钮被点击后更新显示的文本。你将学会：

- **定义事件处理函数：** 事件处理函数接受三个参数，分别是绑定事件时的组件、事件对象、其它参数。
- **绑定事件：** 需要用到 `ui_widget_on()` 函数，它能将事件处理函数与事件绑定，并在事件触发时调用事件处理函数。它接受四个参数，分别是组件、事件名称、事件处理函数、自定义数据。

首先，修改 XML 文件，为需要操作的组件添加 id 属性，以便于在代码中访问它们。

```xml title="main.xml"
<?xml version="1.0" encoding="UTF-8" ?>
<lcui-app>
  <resource type="text/css" src="main.css"/>
  <ui>
    //highlight-next-line
    <text class="title" id="title">
      Welcome to my app
    </text>
    //highlight-next-line
    <my-button id="btn" />
  </ui>
</lcui-app>
```

然后，添加点击（Click）事件处理函数和事件绑定。

```c title="src/main.c"
// highlight-start
void handle_my_button_click(ui_widget_t *w, ui_event_t *e, void *arg)
{
        ui_text_set_content(e->data, "You clicked my button!");
}
// highlight-end

int main(int argc, char *argv[])
{
        lcui_init();
        register_my_button();

        ui_widget_t *result = ui_load_xml_file("main.xml");
        if (result == NULL) {
                return -1;
        }
        ui_root_append(result);
        ui_widget_unwrap(result);
        // highlight-start
        ui_widget_on(ui_get_widget("btn"), "click", handle_my_button_click,
                     ui_get_widget("title"));
        // highlight-end
        return lcui_main();
}
```

import EventHandlerExample from '@site/src/components/QuickStart/EventHandlerExample';

<EventHandlerExample />

:::note
你可以通过查看 ui/types.h 头文件里的 `ui_event_type_t` 的定义来了解其它事件。
:::

## 更新界面

通常你会希望你的组件 “记住” 一些信息并展示出来，比如一个按钮被点击的次数。要做到这一点，你需要在你的组件中添加私有数据，而这些数据也可称为“状态”。

首先，定义组件状态的数据结构：

```c title="src/main.c"
typedef struct {
        int count;
} my_button_t;
```

在初始化函数中初始化状态：

```c title="src/main.c"
void my_button_init(ui_widget_t *w)
{
        my_button_proto->proto->init(w);
        // highlight-start
        my_button_t *data;
        data = ui_widget_add_data(w, my_button_proto, sizeof(my_button_t));
        data->count = 0;
        // highlight-end
}
```

然后，定义一个更新函数，用于将状态转变为用户界面上实际要展示内容：

```c title="src/main.c"
void my_button_update(ui_widget_t *w)
{
        char str[64];
        my_button_t *data = ui_widget_get_data(w, my_button_proto);
        sprintf(str, "Clicked %d times", data->count);
        ui_text_set_content(w, str);
}
```

初始化函数中也需要调用它来更新初始内容：

```c title="src/main.c"
void my_button_init(ui_widget_t *w)
{
        my_button_t *data;

        my_button_proto->proto->init(w);
        ui_text_set_content(w, "I'm a button");
        data = ui_widget_add_data(w, my_button_proto, sizeof(my_button_t));
        data->count = 0;
        // highlight-next-line
        my_button_update(w);
}
```

之后，添加点击事件的处理函数，在里面更改状态并进行更新：

```c title="src/main.c"
// highlight-start
void my_button_on_click(ui_widget_t *w, ui_event_t *e, void *arg)
{
        my_button_t *data;

        data = ui_widget_get_data(w, my_button_proto);
        data->count += 1;
        my_button_update(w);
}
// highlight-end

void my_button_init(ui_widget_t *w)
{
        my_button_t *data;

        data = ui_widget_add_data(w, my_button_proto, sizeof(my_button_t));
        data->count = 0;
        my_button_update(w);
        // highlight-next-line
        ui_widget_on(w, "click", my_button_on_click, NULL);
}
```

最后，删掉 `main()` 中的事件绑定代码，像这样：

```c title="src/main.c"
int main(int argc, char *argv[])
{
        lcui_init();
        register_my_button();

        ui_widget_t *result = ui_load_xml_file("main.xml");
        if (result == NULL) {
                return -1;
        }
        ui_root_append(result);
        ui_widget_unwrap(result);
        return lcui_main();
}
```

你可以在 XML 文件中添加多个 my-button 组件，然后尝试点击不同的按钮：

```xml title="main.xml"
<?xml version="1.0" encoding="UTF-8" ?>
<lcui-app>
  <resource type="text/css" src="main.css"/>
  <ui>
    //highlight-start
    <text class="title">
      Counters that update separately
    </text>
    <my-button />
    <my-button />
    //highlight-end
  </ui>
</lcui-app>
```

import WidgetStateExample from '@site/src/components/QuickStart/WidgetStateExample';

<WidgetStateExample />

你会发现，每个按钮会 “记住” 自己的 count，而不影响其他按钮。

## 下一节

至此，你已经了解了如何编写 LCUI 应用程序的基本知识。接下来你可以查看[教程](../tutorials/todolist)并把它们付诸实践，用 LCUI 建立第一个迷你应用程序。

## 问题反馈

在体验过本章节后，你可能会发现一些问题并且产生了相关的想法和建议，比如，你觉得多次定义函数和获取组件数据太麻烦，像下面样将组件的初始化、更新、事件处理都合并进一个函数内应该能解决问题：

```c
void my_button(ui_widget_t *w)
{
        char str[64];
        my_button_t *data;
        
        data = ui_widget_get_data(my_button_proto);
        if (!data) {
                // 初始化
                data = ui_widget_add_data(w, my_button_proto, sizeof(my_button_t));
                data->count = 0;
        }
        // 事件处理
        if (w->event.type == UI_EVENT_CLICK) {
                data->count += 1;
        }
        // 更新
        sprintf(str, "Clicked %d times", data->count);
        ui_text_set_content(w, str);
}
```

如果你有更多类似的想法和建议，欢迎到 [Gitee](https://gitee.com/lc-soft/LCUI/issues/new) 或 [GitHub](https://github.com/lc-soft/LCUI/issues/new) 代码库主页中提交。
